import {
  EventCenterForMicroApp,
  rebuildDataCenterSnapshot,
  recordDataCenterSnapshot,
} from '../interact';
import globalEnv from '../libs/global_env';
import {
  getEffectivePath,
  isArray,
  isPlainObject,
  isString,
  removeDomScope,
  unique,
  throttleDeferForSetAppName,
  rawDefineProperty,
  rawDefineProperties,
  isFunction,
  rawHasOwnProperty,
  pureCreateElement,
} from '../libs/utils';
import microApp from '../micro_app';
import bindFunctionToRawWindow from './bind_function';
import effect, { effectDocumentEvent, releaseEffectDocumentEvent } from './effect';
import { patchElementPrototypeMethods, releasePatches } from '../source/patch';

// Variables that can escape to rawWindow
const staticEscapeProperties = ['System', '__cjsWrapper'];

// Variables that can only assigned to rawWindow
const escapeSetterKeyList = ['location'];

const globalPropertyList = ['window', 'self', 'globalThis'];

export default class SandBox {
  static activeCount = 0; // number of active sandbox
  recordUmdEffect;
  rebuildUmdEffect;
  releaseEffect;
  /**
   * Scoped global Properties(Properties that can only get and set in microAppWindow, will not escape to rawWindow)
   * https://github.com/micro-zoe/micro-app/issues/234
   */
  scopeProperties = ['webpackJsonp', 'Vue'];
  // Properties that can be escape to rawWindow
  escapeProperties = [];
  // Properties newly added to microAppWindow
  injectedKeys = new Set();
  // Properties escape to rawWindow, cleared when unmount
  escapeKeys = new Set();
  // record injected values before the first execution of umdHookMount and rebuild before remount umd app
  recordUmdInjectedValues;
  // sandbox state
  active = false;
  proxyWindow; // Proxy
  microAppWindow = {}; // Proxy target

  constructor(appName, url) {
    // get scopeProperties and escapeProperties from plugins
    this.getSpecialProperties(appName);
    // create proxyWindow with Proxy(microAppWindow)
    this.proxyWindow = this.createProxyWindow(appName);
    // inject global properties
    this.initMicroAppWindow(this.microAppWindow, appName, url);
    // Rewrite global event listener & timeout
    Object.assign(this, effect(this.microAppWindow));
  }

  start(baseRoute) {
    if (!this.active) {
      this.active = true;
      this.microAppWindow.__MICRO_APP_BASE_ROUTE__ = this.microAppWindow.__MICRO_APP_BASE_URL__ =
        baseRoute;
      // BUG FIX: bable-polyfill@6.x
      globalEnv.rawWindow._babelPolyfill && (globalEnv.rawWindow._babelPolyfill = false);
      if (++SandBox.activeCount === 1) {
        effectDocumentEvent();
        patchElementPrototypeMethods();
      }
    }
  }

  stop() {
    if (this.active) {
      this.active = false;
      this.releaseEffect();
      this.microAppWindow.microApp.clearDataListener();
      this.microAppWindow.microApp.clearGlobalDataListener();

      this.injectedKeys.forEach((key) => {
        Reflect.deleteProperty(this.microAppWindow, key);
      });
      this.injectedKeys.clear();

      this.escapeKeys.forEach((key) => {
        Reflect.deleteProperty(globalEnv.rawWindow, key);
      });
      this.escapeKeys.clear();

      if (--SandBox.activeCount === 0) {
        releaseEffectDocumentEvent();
        releasePatches();
      }
    }
  }

  // record umd snapshot before the first execution of umdHookMount
  recordUmdSnapshot() {
    this.microAppWindow.__MICRO_APP_UMD_MODE__ = true;
    this.recordUmdEffect();
    recordDataCenterSnapshot(this.microAppWindow.microApp);

    this.recordUmdInjectedValues = new Map();
    this.injectedKeys.forEach((key) => {
      this.recordUmdInjectedValues?.set(key, Reflect.get(this.microAppWindow, key));
    });
  }

  // rebuild umd snapshot before remount umd app
  rebuildUmdSnapshot() {
    this.recordUmdInjectedValues?.forEach((value, key) => {
      Reflect.set(this.proxyWindow, key, value);
    });
    this.rebuildUmdEffect();
    rebuildDataCenterSnapshot(this.microAppWindow.microApp);
  }

  /**
   * get scopeProperties and escapeProperties from plugins
   * @param appName app name
   */
  getSpecialProperties(appName) {
    if (!isPlainObject(microApp.plugins)) return;

    this.commonActionForSpecialProperties(microApp.plugins?.global);
    this.commonActionForSpecialProperties(microApp.plugins?.modules?.[appName]);
  }

  // common action for global plugins and module plugins
  commonActionForSpecialProperties(plugins) {
    if (isArray(plugins)) {
      for (const plugin of plugins) {
        if (isPlainObject(plugin)) {
          if (isArray(plugin.scopeProperties)) {
            this.scopeProperties = this.scopeProperties.concat(plugin.scopeProperties);
          }
          if (isArray(plugin.escapeProperties)) {
            this.escapeProperties = this.escapeProperties.concat(plugin.escapeProperties);
          }
        }
      }
    }
  }

  // create proxyWindow with Proxy(microAppWindow)
  createProxyWindow(appName) {
    const rawWindow = globalEnv.rawWindow;
    const descriptorTargetMap = new Map();
    // window.xxx will trigger proxy
    return new Proxy(this.microAppWindow, {
      get: (target, key) => {
        throttleDeferForSetAppName(appName);

        if (
          Reflect.has(target, key) ||
          (isString(key) && /^__MICRO_APP_/.test(key)) ||
          this.scopeProperties.includes(key)
        )
          return Reflect.get(target, key);

        const rawValue = Reflect.get(rawWindow, key);

        return isFunction(rawValue) ? bindFunctionToRawWindow(rawWindow, rawValue) : rawValue;
      },
      set: (target, key, value) => {
        if (this.active) {
          if (escapeSetterKeyList.includes(key)) {
            Reflect.set(rawWindow, key, value);
          } else if (
            // target.hasOwnProperty has been rewritten
            !rawHasOwnProperty.call(target, key) &&
            rawHasOwnProperty.call(rawWindow, key) &&
            !this.scopeProperties.includes(key)
          ) {
            const descriptor = Object.getOwnPropertyDescriptor(rawWindow, key);
            const { configurable, enumerable, writable, set } = descriptor;
            // set value because it can be set
            rawDefineProperty(target, key, {
              value,
              configurable,
              enumerable,
              writable: writable ?? !!set,
            });

            this.injectedKeys.add(key);
          } else {
            Reflect.set(target, key, value);
            this.injectedKeys.add(key);
          }

          if (
            (this.escapeProperties.includes(key) ||
              (staticEscapeProperties.includes(key) && !Reflect.has(rawWindow, key))) &&
            !this.scopeProperties.includes(key)
          ) {
            Reflect.set(rawWindow, key, value);
            this.escapeKeys.add(key);
          }
        }

        return true;
      },
      has: (target, key) => {
        if (this.scopeProperties.includes(key)) return key in target;
        return key in target || key in rawWindow;
      },
      // Object.getOwnPropertyDescriptor(window, key)
      getOwnPropertyDescriptor: (target, key) => {
        if (rawHasOwnProperty.call(target, key)) {
          descriptorTargetMap.set(key, 'target');
          return Object.getOwnPropertyDescriptor(target, key);
        }

        if (rawHasOwnProperty.call(rawWindow, key)) {
          descriptorTargetMap.set(key, 'rawWindow');
          const descriptor = Object.getOwnPropertyDescriptor(rawWindow, key);
          if (descriptor && !descriptor.configurable) {
            descriptor.configurable = true;
          }
          return descriptor;
        }

        return undefined;
      },
      // Object.defineProperty(window, key, Descriptor)
      defineProperty: (target, key, value) => {
        const from = descriptorTargetMap.get(key);
        if (from === 'rawWindow') {
          return Reflect.defineProperty(rawWindow, key, value);
        }
        return Reflect.defineProperty(target, key, value);
      },
      // Object.getOwnPropertyNames(window)
      ownKeys: (target) => {
        return unique(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
      },
      deleteProperty: (target, key) => {
        if (rawHasOwnProperty.call(target, key)) {
          this.injectedKeys.has(key) && this.injectedKeys.delete(key);
          this.escapeKeys.has(key) && Reflect.deleteProperty(rawWindow, key);
          return Reflect.deleteProperty(target, key);
        }
        return true;
      },
    });
  }

  /**
   * inject global properties to microAppWindow
   * @param microAppWindow micro window
   * @param appName app name
   * @param url app url
   */
  initMicroAppWindow(microAppWindow, appName, url) {
    microAppWindow.__MICRO_APP_ENVIRONMENT__ = true;
    microAppWindow.__MICRO_APP_NAME__ = appName;
    microAppWindow.__MICRO_APP_PUBLIC_PATH__ = getEffectivePath(url);
    microAppWindow.__MICRO_APP_WINDOW__ = microAppWindow;
    microAppWindow.microApp = Object.assign(new EventCenterForMicroApp(appName), {
      removeDomScope,
      pureCreateElement,
    });
    microAppWindow.rawWindow = globalEnv.rawWindow;
    microAppWindow.rawDocument = globalEnv.rawDocument;
    microAppWindow.hasOwnProperty = (key) =>
      rawHasOwnProperty.call(microAppWindow, key) ||
      rawHasOwnProperty.call(globalEnv.rawWindow, key);
    this.setMappingPropertiesWithRawDescriptor(microAppWindow);
    this.setHijackProperties(microAppWindow, appName);
  }

  // properties associated with the native window
  setMappingPropertiesWithRawDescriptor(microAppWindow) {
    let topValue, parentValue;
    const rawWindow = globalEnv.rawWindow;
    if (rawWindow === rawWindow.parent) {
      // not in iframe
      topValue = parentValue = this.proxyWindow;
    } else {
      // in iframe
      topValue = rawWindow.top;
      parentValue = rawWindow.parent;
    }

    rawDefineProperty(
      microAppWindow,
      'top',
      this.createDescriptorForMicroAppWindow('top', topValue),
    );

    rawDefineProperty(
      microAppWindow,
      'parent',
      this.createDescriptorForMicroAppWindow('parent', parentValue),
    );

    globalPropertyList.forEach((key) => {
      rawDefineProperty(
        microAppWindow,
        key,
        this.createDescriptorForMicroAppWindow(key, this.proxyWindow),
      );
    });
  }

  createDescriptorForMicroAppWindow(key, value) {
    const {
      configurable = true,
      enumerable = true,
      writable,
      set,
    } = Object.getOwnPropertyDescriptor(globalEnv.rawWindow, key) || { writable: true };
    const descriptor = {
      value,
      configurable,
      enumerable,
      writable: writable ?? !!set,
    };

    return descriptor;
  }

  // set hijack Properties to microAppWindow
  setHijackProperties(microAppWindow, appName) {
    let modifiedEval, modifiedImage;
    rawDefineProperties(microAppWindow, {
      document: {
        get() {
          throttleDeferForSetAppName(appName);
          return globalEnv.rawDocument;
        },
        configurable: false,
        enumerable: true,
      },
      eval: {
        get() {
          throttleDeferForSetAppName(appName);
          return modifiedEval || eval;
        },
        set: (value) => {
          modifiedEval = value;
        },
        configurable: true,
        enumerable: false,
      },
      Image: {
        get() {
          throttleDeferForSetAppName(appName);
          return modifiedImage || globalEnv.ImageProxy;
        },
        set: (value) => {
          modifiedImage = value;
        },
        configurable: true,
        enumerable: false,
      },
    });
  }
}
